
Alkaline.Modules.newModule('5to3', function() {

this.requiredModules = [{
	name: 'Analyzer'
}];

this.registerSupportedArgument('translateArrayComprehension', 'tA', 0, true);
this.registerSupportedArgument('translateConditionalCatch', 'tC', 0, true);
this.registerSupportedArgument('translateForEach', 'tE', 0, true);
this.registerSupportedArgument('translateGenerators', 'tG', 0, true);

var TOKEN = Alkaline.JSInfo.TOKEN;
var tokenNames = Alkaline.JSInfo.tokenNames;
var exampleParser = new AlkalineParser();
this.opIsAssignment = exampleParser.opIsAssignment;

var ProcessingState = this.ProcessingState = function() {
	this.generatorLocals = [];
}

this.run = function(ast) {
	this.treeBuilder = this.compileResult.treeBuilder;
	this.process(ast);
}

this.declTokenTypes = [TOKEN.Var, TOKEN.Let, TOKEN.Const];

/**
 *	AST processing
 */
this.process = function(ast) {
	return this['process' + ast.token.type](ast);
}

this.processARRAY = function(ast) {
	if (ast.children && ast.children.length)
		for (var i = 0; i < ast.children.length; i++)
			this.process(ast.children[i]);
}

this.processArrayComprehension = function(ast) {
	if (!this.arguments.translateArrayComprehension)
		this.process(ast.children[ast.children.length-1]);
	else {
		var expr = ast.children[0];
		var loop = ast.children[1];
		while (loop.children && loop.children.length)
			loop = loop.children[loop.children.length-1];
		var func = this.treeBuilder.Function(null, [], this.treeBuilder.BLOCK([
			ast.children[1]
		]));
		this.treeBuilder.replaceTree(ast, this.treeBuilder.CALL(func, [ this.treeBuilder.ARRAY([]) ]));
		var arrName = func.objectValue.addNewArgument().name;
		this.treeBuilder.addChild(loop, this.treeBuilder.CALL(
			this.treeBuilder.SELECT(
				this.treeBuilder.Id(arrName),
				this.treeBuilder.String('push')
			),
			[ expr ]
		));
		func.objectValue.captureUndeclaredGlobals();
		this.treeBuilder.addChild(func.objectValue.bodyTree, this.treeBuilder.Return(
			this.treeBuilder.Id(arrName)
		));
		this.process(func.parent);	// process the CALL node which replaced the ArrayComprehension tree
	}
}

this.processBLOCK = function(ast) {
	if (!ast.children || !ast.children.length)
		return;	// empty block; nothing to do
	var done = {};
	// first process function declarations
	for (var i = 0; i < ast.children.length; i++)
		if (ast.children[i].token.type == TOKEN.Function) {
			this.process(ast.children[i]);
			done[i] = true;
		}
	// then process the rest of instructions
	for (var i = 0; i < ast.children.length; i++)
		if (!done[i])
			this.process(ast.children[i]);
}

this.processCALL = function(ast) {
	for (var i = 0; i < ast.children.length; i++)
		this.process(ast.children[0]);
}

this.processCatch = function(ast) {
	this.process(ast.children[ast.children.length-1]);
}

this.processDelete = function(ast) {
	this.process(ast.children[0]);
}

this.processEXPR = function(ast) {
	for (var i = 0; i < ast.children.length; i++)
		this.process(ast.children[i]);
}

this.processFor = function(ast) {
	var Each = ast.children[0].token.type === TOKEN.Each;
	if (Each && this.arguments.translateForEach) {
		this.treeBuilder.removeTree(ast.children[0]);
		var iteratorRef = ast.children[0].children[0].objectValue;
		var eachRef = ast.savedState.currentContext.varContext.getNewVariableAvailableInChildren(false);
		iteratorRef.removeReference(ast.children[0].children[0]);
		eachRef.addReference(ast.children[0].children[0]);
		ast.children[0].children[0].token.text = eachRef.name;
		if (ast.children[1].token.type !== TOKEN.BLOCK) {
			var child = ast.children[1];
			this.treeBuilder.removeTree(ast.children[1]);
			this.treeBuilder.addChild(ast, this.treeBuilder.BLOCK([ child ]));
		}
		var target;
		if (ast.children[0].children[1].token.type === TOKEN.Id)
			target = ast.children[0].children[1];
		else if (ast.children[0].children[1].token.type === TOKEN.Operator && ast.children[0].children[1].token.text === '=' && ast.children[0].children[1].children[0].token.type === TOKEN.Id)
			target = ast.children[0].children[1].children[0];
		else {
			// assign the object which is being iterated to a variable
			target = this.treeBuilder.Id(ast.savedState.currentContext.varContext.getNewVariableAvailableInChildren(false).name);
			var expr = ast.children[0].children[1];
			ast.children[0].deleteChild(1);
			this.treeBuilder.addChild(ast.children[0], this.treeBuilder.Operator(
				'=',
				target,
				expr
			));
		}
		target = target.token.text;
		this.treeBuilder.addChild(ast.children[1],
			this.treeBuilder.Operator(
				'=',
				this.treeBuilder.Id(iteratorRef.name),
				this.treeBuilder.SELECT(
					this.treeBuilder.Id(target),
					this.treeBuilder.Id(eachRef.name)
				)
			), 0);
		Each = false;
	}
	for (var i = +Each; i < ast.children.length; i++)
		this.process(ast.children[i]);
}

this.processFunction = function(ast) {
	var functionObject = ast.objectValue;
	if (functionObject.isGenerator && this.arguments.translateGenerators) {
		// first, if the generator is a function declaration and not inside an expression, then use an assignment
		if (ast.isFunctionDeclaration) {
			ast.isFunctionDeclaration = false;
			var name = ast.children[0].token.text;
			this.treeBuilder.setChild(ast.parent, ast.childIndex, this.treeBuilder.Operator(
				'=',
				this.treeBuilder.Id(name),
				ast
			));
			// declare the variable
			var ctx = ast.objectValue.innerLexicalContext.inheritedContext;
			var varRef = ctx.getVariable(name);
			if (varRef.declarationToken === ast.children[0])
				varRef.declarationToken = null;
			varRef.declare();
		}
		
		var argsAST = ast.children[ast.children.length-2];
		var generatorArgs = [];
		// make the array of arguments
		if (argsAST.children) {
			for (var i = 0; i < argsAST.children.length; i++)
				generatorArgs.push(this.treeBuilder.String(argsAST.children[i].token.text));
		}
		var generatorArgs = this.treeBuilder.ARRAY(generatorArgs);
		
		this.treeBuilder.replaceTree(ast, this.treeBuilder.CALL(
			this.treeBuilder.getTreePath(['Alkaline', 'Runtime', 'Generator', 'newInstance']),
			[generatorArgs]
		)).addChild(ast);
		
		var locals = functionObject.innerLexicalContext.getLocalVariables();
		
		var thisVar = functionObject.addNewArgument();
		
		// replace all references to local variables; remove variable declarations
		for (var i = 0; i < locals.length; i++)
			if (locals[i] !== thisVar) {
				var ref = locals[i].referencedFrom;
				for (var j = ref.length-1; j >= 0; j--)
					this.treeBuilder.replaceTree(ref[j], this.treeBuilder.getTreePath([thisVar.name, '_local_' + locals[i].name]));
				ref.splice(0, ref.length);
				if (locals[i].sourceType !== Alkaline.Processing.VariableDefinition.SOURCE_TYPE.NATIVE)
					functionObject.innerLexicalContext.removeVariable(locals[i]);
			}

		var oThis = this;
		var guideCondition = function(condition, continuation) {
			var parent = condition.parent;
			var index = condition.childIndex;
			var firstGuide = !parent._5to3_guidedCondition;
			parent._5to3_guidedCondition = true;
			if (condition.children && condition.children.length == 1)
				condition = condition.children[0];
			oThis.treeBuilder.setChild(parent, index, oThis.treeBuilder.EXPR(oThis.treeBuilder.Operator(
					'||',
					oThis.treeBuilder.Operator(
						'===',
						oThis.treeBuilder.getTreePath([thisVar.name, '_continuation']),
						oThis.treeBuilder.Number(continuation+1)
					),
					firstGuide ?
						oThis.treeBuilder.Operator(
							'&&',
							oThis.treeBuilder.Operator(
								'<',
								oThis.treeBuilder.getTreePath([thisVar.name, '_continuation']),
								oThis.treeBuilder.Number(1)
							),
							condition
						) :
					condition
				)));
		}
		var guideBranch = function(condition, continuation, branch) {
			var parent = condition.parent;
			var index = condition.childIndex;
			if (condition.children.length == 1)
				condition = condition.children[0];
			var parent = yieldParent.parent;
			oThis.treeBuilder.setChild(parent, 0, oThis.treeBuilder.QMark(
				oThis.treeBuilder.Operator(
					'===',
					oThis.treeBuilder.getTreePath([thisVar.name, '_continuation']),
					oThis.treeBuilder.Number(continuation+1)
				),
				branch,
				condition
			));
		}

		// place conditionals to guide execution
		var innerBlock = ast.children[ast.children.length-1];
		var yieldContainer = [];
		for (var i = 0; i < functionObject.yields.length; i++) {
			var stmt = functionObject.yields[i];
			var yieldParent = yieldContainer[i] = stmt = Alkaline.JSInfo.getExpressionRoot(stmt);
			var bracketStmts = true;
			while (yieldParent != innerBlock) {
				if (bracketStmts && yieldParent.childIndex && !this.treeBuilder.isControlStructure(yieldParent.parent)) {
					var newParent = yieldParent.parent;
					var first = yieldParent.childIndex;
					while (first)
						if (!newParent.children[first-1].doneYieldGuide)
							first--;
						else {
							newParent.children[first-1].doneYieldGuide = 2;
							bracketStmts = false;
							break;
						}
					if (first === 0 && this.declTokenTypes.contains(newParent.children[0].token.type))
						first++;
					if (first < yieldParent.childIndex) {
						var innerStmts = [];
						for (var j = first; j < yieldParent.childIndex; j++)
							innerStmts.push(newParent.children[j]);
						this.treeBuilder.replaceChildren(newParent, first, yieldParent.childIndex-1, this.treeBuilder.If(
							this.treeBuilder.Operator(
								'<',
								this.treeBuilder.getTreePath([thisVar.name, '_continuation']),
								this.treeBuilder.Number(i+1)
							),
							this.treeBuilder.BLOCK(innerStmts)
						));
						newParent.children[first].doneYieldGuide = true;
					}
				}
				yieldParent.doneYieldGuide = 1;
				if (yieldParent.parent.token.type === TOKEN.For && yieldParent.childIndex > 2) {
					if (bracketStmts) {
						// first expr is the "for"-initializer: execute it only during normal run
						var oldExpr = yieldParent.parent.children[0];
						if (oldExpr.children.length == 1)
							oldExpr = oldExpr.children[0];
						this.treeBuilder.setChild(yieldParent.parent, 0, this.treeBuilder.Operator(
							'&&',
							this.treeBuilder.Operator(
								'<=',
								this.treeBuilder.getTreePath([thisVar.name, '_continuation']),
								this.treeBuilder.Number(0)
							),
							oldExpr
						));
					}

					// second expr is the "for"-condition: force it true if we are skipping to a position inside
					guideCondition(yieldParent.parent.children[1], i);

				} else if (yieldParent.parent.token.type === TOKEN.While && yieldParent.childIndex > 0)
					guideCondition(yieldParent.parent.children[0], i);
				else if (yieldParent.parent.token.type === TOKEN.If && yieldParent.childIndex) {
					var branch = yieldParent.childIndex === 1 ? this.treeBuilder.True() : this.treeBuilder.False();
					guideBranch(yieldParent.parent.children[0], i, branch);
				}
				yieldParent = yieldParent.parent;
			}
		}

		// replace yield instructions
		for (var i = 0; i < functionObject.yields.length; i++) {
			// if (X._continuation < n) { X._continuation = n; return ...; } else if (X._continuation === n) X._continuation = -1;
			// also keep the expression inside which the yield was, if there is one
			var yld = functionObject.yields[i];
			var expr = yieldContainer[i];
			
			// list with stuff to do when reinvoked after this yield
			var onResume = [];
			// restore _continuation
			//if (yld.doneYieldGuide > 1)
				onResume.push(this.treeBuilder.Operator(
					'=',
					this.treeBuilder.getTreePath([thisVar.name, '_continuation']),
					this.treeBuilder.Number(-1)
				));
			// read input from the other side (the generator could also be requested to throw an exception)
			var yieldReturn = this.treeBuilder.CALL(this.treeBuilder.getTreePath(['this', '_yieldReturn']));
			if (yld !== expr) {
				onResume.push(expr);
				this.treeBuilder.replaceTree(yld, yieldReturn);
			} else
				onResume.push(yieldReturn);
			
			// apply the changes to the AST
			this.treeBuilder.setChild(expr.parent, expr.childIndex,
				this.treeBuilder.If(
					this.treeBuilder.Operator(
						"<",
						this.treeBuilder.getTreePath([thisVar.name, '_continuation']),
						this.treeBuilder.Number(i+1)
					),
					this.treeBuilder.BLOCK([
						this.treeBuilder.Operator(
							'=',
							this.treeBuilder.getTreePath([thisVar.name, '_continuation']),
							this.treeBuilder.Number(i+1)
						),
						this.treeBuilder.Return(
							functionObject.yields[i].children[0]
						)
					]),
					this.treeBuilder.If(
						this.treeBuilder.Operator(
							"===",
							this.treeBuilder.getTreePath([thisVar.name, '_continuation']),
							this.treeBuilder.Number(i+1)
						),
						this.treeBuilder.BLOCK(onResume)
					)
				)
			);
		}
	}
	this.process(ast.children[ast.children.length-1]);
}

this.processIf = function(ast) {
	for (var i = 0; i < ast.children.length; i++)
		this.process(ast.children[i]);
}

this.processITEM = function(ast) {
	this.process(ast.children[0]);
}

this.processNew = function(ast) {
	for (var i = 0; i < ast.children.length; i++)
		this.process(ast.children[i]);
}

this.processReturn = function(ast) {
	if (ast.children && ast.children.length)
		for (var i = 0; i < ast.children.length; i++)
			this.process(ast.children[i]);
}

this.processSELECT = function(ast) {
	this.process(ast.children[0]);
}

this.processThrow = function(ast) {
	for (var i = 0; i < ast.children.length; i++)
		this.process(ast.children[i]);
}

this.processTry = function(ast) {
	if (this.arguments.translateConditionalCatch) {
		//
		//	translate conditional catch
		//
		
		if (ast.children[1].token.type == TOKEN.Catch && ast.children[1].children.length > 2) {
			var excVar, catchAll = null;
			var endFinally = ast.children[ast.children.length-1].token.type == TOKEN.Finally;
			var child = ast.children[ast.children.length-1-endFinally];
			if (child.token.type === TOKEN.Catch && child.children.length < 3)
				catchAll = child;
			else {
				excVar = ast.children[1].children[0].token.text;
				catchAll = this.treeBuilder.Catch(excVar, this.treeBuilder.BLOCK( [
					this.treeBuilder.Throw(
						this.treeBuilder.Id(excVar)
					)
				] ));
				this.treeBuilder.addChild(
					ast,
					catchAll,
					ast.children[ast.children.length-1].token.type == TOKEN.Finally ? ast.children.length-1 : null
				);
			}
			excVar = catchAll.exceptionVariable;
			var lastIdx = ast.children.length-2-endFinally;
			for (i = lastIdx; i >= 1; i--) {
				ast.children[i].exceptionVariable.renameAllReferences(excVar);
				catchAll.replaceChildren(1, 1, this.treeBuilder.If(
					ast.children[i].children[1],
					ast.children[i].children[2],
					catchAll.children[1]
				));
			}
			catchAll.replaceChildren(1, 1, this.treeBuilder.BLOCK( [
				catchAll.children[1]
			] ));
			ast.replaceChildren(1, lastIdx, this.treeBuilder.nil());	// remove conditional catch statements
		}
	}
	for (var i = 0; i < ast.children.length; i++)
		this.process(ast.children[i]);
}

this.processWhile = function(ast) {
	this.process(ast.children[0]);
	this.process(ast.children[1]);
}

this.processId =
this.processNumber =
this.processString =
this.processTrue =
this.processVar = function(ast) {
	// do nothing. really
}

this.processEqual =
this.processIn =
this.processInstanceOf =
this.processOperator = function(ast, state) {
	for (var i = 0; i < ast.children.length; i++)
		this.process(ast.children[i], state);
}


this._processNotImplemented = function(ast, state) {
	this.compileResult.stdWarn('[5to3] Processing of ' + Alkaline.JSInfo.tokenName[ast.token.type] + ' is not implemented: %o.', ast);
}

for (var i = 0; i < tokenNames.length; i++)
	this['process' + TOKEN[tokenNames[i]]] = this['process' + tokenNames[i]] || (this['process' + tokenNames[i]] = this._processNotImplemented);

});
